/*
 * Copyright 2013-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.cloud.gateway.filter.headers;

import java.net.URI;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;

/**
 * XForwardedHeadersFilter 是 Spring Cloud Gateway 中的一个过滤器，
 * 用于处理非标准化的 X-Forwarded-* 系列头（如 X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Host 等）。
 * 这些头通常由反向代理（如 Nginx、HAProxy 或负载均衡器）添加，
 * 用于传递客户端的真实请求信息（如 IP 地址、协议、主机名等），以便后端服务能够正确识别客户端。
 *
 * XForwardedHeadersFilter 主要用于封装 the X-Forwarded-* 系列头信息，
 * XForwardedHeadersFilter 的主要目的是解析上述 X-Forwarded-* 系列头，并将相关信息存储到 Spring 的 ServerWebExchange 对象中，以便下游过滤器或服务能够访问这些信息。
 *
 * 在网关上配置
 * spring:
 *   cloud:
 *     gateway:
 *       x-forwarded:
 *         enabled: true
 *         forEnabled: true
 *         hostEnabled: true
 *         portEnabled: true
 *         protoEnabled: true
 *         prefixEnabled: true
 *
 	enabled：是否启用 XForwardedHeadersFilter。
 	headers-to-process：指定需要解析的头列表。
 */
@ConfigurationProperties("spring.cloud.gateway.x-forwarded")
public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {

	/** Default http port. */
	public static final int HTTP_PORT = 80;

	/** Default https port. */
	public static final int HTTPS_PORT = 443;

	/** Http url scheme. */
	public static final String HTTP_SCHEME = "http";

	/** Https url scheme. */
	public static final String HTTPS_SCHEME = "https";

	/** X-Forwarded-For Header. */
	public static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For";

	/** X-Forwarded-Host Header. */
	public static final String X_FORWARDED_HOST_HEADER = "X-Forwarded-Host";

	/** X-Forwarded-Port Header. */
	public static final String X_FORWARDED_PORT_HEADER = "X-Forwarded-Port";

	/** X-Forwarded-Proto Header. */
	public static final String X_FORWARDED_PROTO_HEADER = "X-Forwarded-Proto";

	/** X-Forwarded-Prefix Header. */
	public static final String X_FORWARDED_PREFIX_HEADER = "X-Forwarded-Prefix";

	/** The order of the XForwardedHeadersFilter. */
	private int order = 0;

	/** If the XForwardedHeadersFilter is enabled. */
	private boolean enabled = true;

	/** If X-Forwarded-For is enabled. */
	private boolean forEnabled = true;

	/** If X-Forwarded-Host is enabled. */
	private boolean hostEnabled = true;

	/** If X-Forwarded-Port is enabled. */
	private boolean portEnabled = true;

	/** If X-Forwarded-Proto is enabled. */
	private boolean protoEnabled = true;

	/** If X-Forwarded-Prefix is enabled. */
	private boolean prefixEnabled = true;

	/** If appending X-Forwarded-For as a list is enabled. */
	private boolean forAppend = true;

	/** If appending X-Forwarded-Host as a list is enabled. */
	private boolean hostAppend = true;

	/** If appending X-Forwarded-Port as a list is enabled. */
	private boolean portAppend = true;

	/** If appending X-Forwarded-Proto as a list is enabled. */
	private boolean protoAppend = true;

	/** If appending X-Forwarded-Prefix as a list is enabled. */
	private boolean prefixAppend = true;

	@Override
	public int getOrder() {
		return this.order;
	}

	public void setOrder(int order) {
		this.order = order;
	}

	public boolean isEnabled() {
		return enabled;
	}

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	public boolean isForEnabled() {
		return forEnabled;
	}

	public void setForEnabled(boolean forEnabled) {
		this.forEnabled = forEnabled;
	}

	public boolean isHostEnabled() {
		return hostEnabled;
	}

	public void setHostEnabled(boolean hostEnabled) {
		this.hostEnabled = hostEnabled;
	}

	public boolean isPortEnabled() {
		return portEnabled;
	}

	public void setPortEnabled(boolean portEnabled) {
		this.portEnabled = portEnabled;
	}

	public boolean isProtoEnabled() {
		return protoEnabled;
	}

	public void setProtoEnabled(boolean protoEnabled) {
		this.protoEnabled = protoEnabled;
	}

	public boolean isPrefixEnabled() {
		return prefixEnabled;
	}

	public void setPrefixEnabled(boolean prefixEnabled) {
		this.prefixEnabled = prefixEnabled;
	}

	public boolean isForAppend() {
		return forAppend;
	}

	public void setForAppend(boolean forAppend) {
		this.forAppend = forAppend;
	}

	public boolean isHostAppend() {
		return hostAppend;
	}

	public void setHostAppend(boolean hostAppend) {
		this.hostAppend = hostAppend;
	}

	public boolean isPortAppend() {
		return portAppend;
	}

	public void setPortAppend(boolean portAppend) {
		this.portAppend = portAppend;
	}

	public boolean isProtoAppend() {
		return protoAppend;
	}

	public void setProtoAppend(boolean protoAppend) {
		this.protoAppend = protoAppend;
	}

	public boolean isPrefixAppend() {
		return prefixAppend;
	}

	public void setPrefixAppend(boolean prefixAppend) {
		this.prefixAppend = prefixAppend;
	}

	/**
	 * 该函数是Spring Cloud Gateway项目中的一个HTTP头部过滤器方法，用于处理并更新传入的HTTP请求头。
	 * 它根据配置和请求的具体信息，可能会添加或修改X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Prefix、X-Forwarded-Port和X-Forwarded-Host等头部信息。
	 * 这个函数是网关服务中处理请求头部逻辑的重要环节，确保了请求在经过网关时，能够正确地转发和记录相关的头部信息。
	 *
	 * 代码逐行解释如下：
	 * 1、首先，方法接收一个HttpHeaders对象input和一个ServerWebExchange对象exchange作为参数，分别代表原始的HTTP头部信息和当前的服务器Web交换上下文。
	 * 通过exchange.getRequest()获取当前的ServerHttpRequest对象，即请求对象。
	 * 创建一个新的HttpHeaders对象updated，用于存放更新后的头部信息。
	 * 遍历原始的头部信息original，将其全部复制到updated中。
	 * 判断是否启用了X-Forwarded-For头部的更新，并且请求的远程地址不为空，则将远程地址的IP添加到X-Forwarded-For头部中。
	 * 获取请求的协议（如HTTP、HTTPS），如果启用了X-Forwarded-Proto头部的更新，则将协议添加到X-Forwarded-Proto头部中。
	 * 如果启用了前缀的更新，则从交换上下文中获取原始请求的URL和当前请求的URL，尝试从原始URL中提取前缀，并更新到X-Forwarded-Prefix头部中。
	 * 判断是否启用了端口的更新，获取请求的端口号，如果端口号为负数（表示未指定），则使用默认端口。将端口号添加到X-Forwarded-Port头部中。
	 * 如果启用了主机的更新，则将请求的主机信息添加到X-Forwarded-Host头部中。
	 * 最后，返回更新后的HttpHeaders对象updated。
	 *
	 * 该函数的核心逻辑是根据配置和请求的具体信息，对HTTP请求头进行必要的添加或修改，以确保请求在经过网关时能够携带正确的转发信息。
	 * 这在网关服务中至关重要，因为它涉及到请求的正确路由、安全性的维护以及后端服务的正确识别和处理。
	 * 在实际业务中，这个函数的作用主要体现在以下几个方面：
	 *
	 * 保持请求的完整性：通过添加或修改请求头，确保请求在经过网关后仍然能够保留原始请求的关键信息。
	 * 提高安全性：通过记录请求的来源IP、协议等信息，有助于追踪和防范潜在的安全威胁。
	 * 支持复杂的路由逻辑：通过添加前缀、端口和主机等信息，支持更复杂的路由和转发逻辑，满足不同的业务需求。
	 * 从项目的角度来看，这个函数是Spring Cloud Gateway项目中处理HTTP请求头的重要组件，
	 * 它的正确实现和配置对于整个网关服务的稳定性和安全性具有重要意义。

	 * @param input Http Headers
	 * @param exchange a {@link ServerWebExchange} that should be filtered
	 * @return
	 */
	@Override
	public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) {

		ServerHttpRequest request = exchange.getRequest();
		HttpHeaders original = input;
		HttpHeaders updated = new HttpHeaders();

		for (Map.Entry<String, List<String>> entry : original.entrySet()) {
			updated.addAll(entry.getKey(), entry.getValue());
		}

		if (isForEnabled() && request.getRemoteAddress() != null && request.getRemoteAddress().getAddress() != null) {
			String remoteAddr = request.getRemoteAddress().getAddress().getHostAddress();
			write(updated, X_FORWARDED_FOR_HEADER, remoteAddr, isForAppend());
		}

		String proto = request.getURI().getScheme();
		if (isProtoEnabled()) {
			write(updated, X_FORWARDED_PROTO_HEADER, proto, isProtoAppend());
		}

		if (isPrefixEnabled()) {
			// If the path of the url that the gw is routing to is a subset
			// (and ending part) of the url that it is routing from then the difference
			// is the prefix e.g. if request original.com/prefix/get/ is routed
			// to routedservice:8090/get then /prefix is the prefix
			// - see XForwardedHeadersFilterTests, so first get uris, then extract paths
			// and remove one from another if it's the ending part.

			LinkedHashSet<URI> originalUris = exchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
			URI requestUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);

			if (originalUris != null && requestUri != null) {

				originalUris.forEach(originalUri -> {

					if (originalUri != null && originalUri.getPath() != null) {
						String prefix = originalUri.getPath();

						// strip trailing slashes before checking if request path is end
						// of original path
						String originalUriPath = stripTrailingSlash(originalUri);
						String requestUriPath = stripTrailingSlash(requestUri);

						updateRequest(updated, originalUri, originalUriPath, requestUriPath);

					}
				});
			}
		}

		if (isPortEnabled()) {
			String port = String.valueOf(request.getURI().getPort());
			if (request.getURI().getPort() < 0) {
				port = String.valueOf(getDefaultPort(proto));
			}
			write(updated, X_FORWARDED_PORT_HEADER, port, isPortAppend());
		}

		if (isHostEnabled()) {
			String host = toHostHeader(request);
			write(updated, X_FORWARDED_HOST_HEADER, host, isHostAppend());
		}

		return updated;
	}

	private void updateRequest(HttpHeaders updated, URI originalUri, String originalUriPath, String requestUriPath) {
		String prefix;
		if (requestUriPath != null && (originalUriPath.endsWith(requestUriPath))) {
			prefix = substringBeforeLast(originalUriPath, requestUriPath);
			if (prefix != null && prefix.length() > 0 && prefix.length() <= originalUri.getPath().length()) {
				write(updated, X_FORWARDED_PREFIX_HEADER, prefix, isPrefixAppend());
			}
		}
	}

	private static String substringBeforeLast(String str, String separator) {
		if (ObjectUtils.isEmpty(str) || ObjectUtils.isEmpty(separator)) {
			return str;
		}
		int pos = str.lastIndexOf(separator);
		if (pos == -1) {
			return str;
		}
		return str.substring(0, pos);
	}

	private void write(HttpHeaders headers, String name, String value, boolean append) {
		if (value == null) {
			return;
		}
		if (append) {
			headers.add(name, value);
			// these headers should be treated as a single comma separated header
			//这个请求头应该 视为由 单个逗号分割的请求头
			List<String> values = headers.get(name);
			String delimitedValue = StringUtils.collectionToCommaDelimitedString(values);
			headers.set(name, delimitedValue);
		}
		else {
			headers.set(name, value);
		}
	}

	private int getDefaultPort(String scheme) {
		return HTTPS_SCHEME.equals(scheme) ? HTTPS_PORT : HTTP_PORT;
	}

	private boolean hasHeader(ServerHttpRequest request, String name) {
		HttpHeaders headers = request.getHeaders();
		return headers.containsKey(name) && StringUtils.hasLength(headers.getFirst(name));
	}

	private String toHostHeader(ServerHttpRequest request) {
		int port = request.getURI().getPort();
		String host = request.getURI().getHost();
		String scheme = request.getURI().getScheme();
		if (port < 0 || (port == HTTP_PORT && HTTP_SCHEME.equals(scheme))
				|| (port == HTTPS_PORT && HTTPS_SCHEME.equals(scheme))) {
			return host;
		}
		else {
			return host + ":" + port;
		}
	}

	private String stripTrailingSlash(URI uri) {
		if (uri.getPath().endsWith("/")) {
			return uri.getPath().substring(0, uri.getPath().length() - 1);
		}
		else {
			return uri.getPath();
		}
	}

}
